<?
/**
 * Copyright 2007 Melange.
 *
 * This file is part of PHP-AAS.
 *
 * PHP-AAS is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * PHP-AAS is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with PHP-AAS; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * @category    Melange
 * @package     php-aas
 * @subpackage  authenticator
 * @copyright   Copyright (c) 2007 Jeroen Simons. All rights reserved
 * @author      Jeroen Simons <jeroen@melange.nl>
 * @link        http://www.melange.nl/
 *
 */

require_once("aas/authenticator/AuthenticatorBase.php");

/**
 * An <b>Authenticator</b> and <b>Valve</b> implementation of FORM BASED
 * Authentication, as described in the Servlet API Specification, Version 2.2.
 */

class FormAuthenticator extends AuthenticatorBase {


    /**
     * Logger
     */
    protected $log;


    // ----------------------------------------------------- Instance Variables


    /**
     * Character encoding to use to read the username and password parameters
     * from the request. If not set, the encoding of the request body will be
     * used.
     */
    protected $characterEncoding = null;


    // ------------------------------------------------------------- Properties


    public function __construct() {
        $this->log = LoggerManager::getLogger("FormAuthenticator");
    }


    /**
     * Return the character encoding to use to read the username and password.
     */
    public function getCharacterEncoding() {
        return $this->characterEncoding;
    }


    /**
     * Set the character encoding to be used to read the username and password.
     */
    public function setCharacterEncoding($encoding) {
        $this->characterEncoding = $encoding;
    }


    // --------------------------------------------------------- Public Methods


    /**
     * Authenticate the user making this request, based on the specified
     * login configuration.  Return <code>true</code> if any specified
     * constraint has been satisfied, or <code>false</code> if we have
     * created a response challenge already.
     *
     * @param request Request we are processing
     * @param response Response we are creating
     * @param config    Login configuration describing how authentication
     *              should be performed
     *
     * @exception IOException if an input/output error occurs
     */
    public function authenticate(Request $request,
                                Response &$response,
                                LoginConfig $config) {

        $session = null;

        // Have we already authenticated someone?
        $principal = $request->getUserPrincipal();

        if ($principal !== null) {
            if ($this->log->isDebugEnabled())
                $this->log->debug("Already authenticated '" .
                    $principal->getName() . "'");
            return (true);
        }

        // Have we authenticated this user before but have caching disabled?
        if (!$this->cache) {
            $session = $request->getSession(true);
            if ($this->log->isDebugEnabled())
                $this->log->debug("Checking for reauthenticate in session " . $session);

            $username =
                $session->getNote(self::SESS_USERNAME_NOTE);
            $password =
                $session->getNote(self::SESS_PASSWORD_NOTE);

            if (($username !== null) && ($password !== null)) {
                if ($this->log->isDebugEnabled())
                    $this->log->debug("Reauthenticating username '" . $username . "'");

                $principal =
                    $this->context->getRealm()->authenticate($username, $password);

                if ($principal !== null) {
                    $session->setNote(self::FORM_PRINCIPAL_NOTE, $principal);
                    if (!$this->matchRequest($request)) {
                        $this->register($request, $response, $principal,
                                 self::FORM_METHOD,
                                 $username, $password);
                        return (true);
                    }
                }
                if ($this->log->isDebugEnabled())
                    $this->log->debug("Reauthentication failed, proceed normally");
            }
        }

        // Is this the re-submit of the original request URI after successful
        // authentication?  If so, forward the *original* request instead.
        if ($this->matchRequest($request)) {
            $session = $request->getSession(true);
            if ($this->log->isDebugEnabled())
                $this->log->debug("Restore request from session '" . $session->getId() . "'");

            $principal = $session->getNote(self::FORM_PRINCIPAL_NOTE);

            $this->register($request, $response, $principal, self::FORM_METHOD,
                     $session->getNote(self::SESS_USERNAME_NOTE),
                     $session->getNote(self::SESS_PASSWORD_NOTE));

            // If we're caching principals we no longer need the username
            // and password in the session, so remove them
            if ($this->cache) {
                if ($this->log->isDebugEnabled())
                    $this->log->debug("Caching so removing username and password from session.");
                $session->removeNote(self::SESS_USERNAME_NOTE);
                $session->removeNote(self::SESS_PASSWORD_NOTE);
            }
            if ($this->restoreRequest($request, $session)) {
                if ($this->log->isDebugEnabled())
                    $this->log->debug("Proceed to restored request");
                return (true);
            } else {
                if ($this->log->isDebugEnabled())
                    $this->log->debug("Restore of original request failed");
                $response->sendError(Response::SC_BAD_REQUEST);
                return (false);
            }
        }

        // Acquire references to objects we will need to evaluate
        $contextPath = $request->getContextPath();
        $requestURI = $request->getRequestUri();
        $response->setContext($request->getContext());

        // Is this a request for the login page itself?  Test here to avoid
        // displaying it twice (from the user's perspective) -- once because
        // of the "save and redirect" and once because of the "restore and
        // redirect" performed below.
        $loginURI = $contextPath . $config->getLoginPage();

        if ($requestURI == $loginURI) {
            if ($this->log->isDebugEnabled())
                $this->log->debug("Requesting login page normally");
            return (true);      // Display the login page in the usual manner
        }

        // Is this a request for the error page itself?  Test here to avoid
        // an endless loop (back to the login page) if the error page is
        // within the protected area of our security constraint
        $errorURI = $contextPath . $config->getErrorPage();
        if ($requestURI == $errorURI) {
            if ($this->log->isDebugEnabled())
                    $this->log->debug("Requesting error page normally");
            return (true);      // Display the error page in the usual manner
        }

        // Is this the action request from the login page?
        $loginAction =
            preg_match("/\\".self::FORM_ACTION."$/", $requestURI);

        // No -- Save this request and redirect to the form login page
        if (!$loginAction) {

            $session = $request->getSession(true);

            if ($this->log->isDebugEnabled())
                $this->log->debug("Save request in session '" . $session->getId() . "'");

            $this->saveRequest($request, $session);

            if ($this->log->isDebugEnabled())
                $this->log->debug("Redirect to login page '" . $loginURI . "'");

            $response->sendRedirect($response->encodeRedirectURL($loginURI));
            return (false);
        }

        // Yes -- Validate the specified credentials and redirect
        // to the error page if they are not correct
        $realm = $this->context->getRealm();

        if ($this->characterEncoding !== null) {
            $request->setCharacterEncoding($this->characterEncoding);
        }
        $username = $request->getParameter(self::FORM_USERNAME);
        $password = $request->getParameter(self::FORM_PASSWORD);

        if ($this->log->isDebugEnabled())
            $this->log->debug("Authenticating username '" . $username . "'");

        $principal = $realm->authenticate($username, $password);

        if ($principal === null) {
            if ($this->log->isDebugEnabled())
                $this->log->debug("Redirect to error page '" . $errorURI . "'");
            $response->sendRedirect($response->encodeRedirectURL($errorURI));
            return (false);
        }

        if ($this->log->isDebugEnabled())
            $this->log->debug("Authentication of '" . $username . "' was successful");

        if ($session === null)
            $session = $request->getSession(false);

        if ($session === null) {
            if ($this->log->isDebugEnabled())
                $this->log->debug("User took so long to log on the session expired");
            $response->sendError(Response::SC_REQUEST_TIMEOUT,
                "The time allowed for the login process has been exceeded. " .
                "If you wish to continue you must either click back twice " .
                "and re-click the link you requested or close and re-open your browser");
            return (false);
        }

        // Save the authenticated Principal in our session
        $session->setNote(self::FORM_PRINCIPAL_NOTE, $principal);

        // Save the username and password as well
        $session->setNote(self::SESS_USERNAME_NOTE, $username);
        $session->setNote(self::SESS_PASSWORD_NOTE, $password);

        // Redirect the user to the original request URI (which will cause
        // the original request to be restored)
        $requestURI = $this->savedRequestURL($session);

        if ($this->log->isDebugEnabled())
            $this->log->debug("Redirecting to original '" . $requestURI . "'");

        if ($requestURI === null) {
            $response->sendError(Response::SC_BAD_REQUEST,
                "Invalid direct reference to form login page");
        } else {
            $response->sendRedirect($response->encodeRedirectURL($requestURI));
        }
        
        return (false);

    }


    // ------------------------------------------------------ Protected Methods


    /**
     * Does this request match the saved one (so that it must be the redirect
     * we signalled after successful authentication?
     *
     * @param request The request to be verified
     */
    protected function matchRequest(Request $request) {

        // Has a session been created?
        $session = $request->getSession(false);
        if ($session === null) {
            if($this->log->isDebugEnabled())
                $this->log->debug("  No session");
            return (false);
        }

        // Is there a saved request?
        require_once("aas/authenticator/SavedRequest.php");
        $sreq = $session->getNote(self::FORM_REQUEST_NOTE);
        if ($sreq === null) {
            if($this->log->isDebugEnabled())
                $this->log->debug("  No saved request (session note)");
            return (false);
        }

        // Is there a saved principal?
        if ($session->getNote(self::FORM_PRINCIPAL_NOTE) === null) {
            if($this->log->isDebugEnabled())
                $this->log->debug("  No saved principal (session note)");
            return (false);
        }

        // Does the request URI match?
        $requestURI = $request->getRequestUri();

        if($this->log->isDebugEnabled())
            $this->log->debug("Matching " . $requestURI . " to saved request " . $sreq->getRequestUri());

        if ($requestURI === null)
            return (false);

        return ($requestURI == $sreq->getRequestUri());

    }


    /**
     * Restore the original request from information stored in our session.
     * If the original request is no longer present (because the session
     * timed out), return <code>false</code>; otherwise, return
     * <code>true</code>.
     *
     * @param request The request to be restored
     * @param session The session containing the saved information
     */
    protected function restoreRequest($request, $session) {

        // Retrieve and remove the SavedRequest object from our session
        $saved = $session->getNote(self::FORM_REQUEST_NOTE);
        $session->removeNote(self::FORM_REQUEST_NOTE);
        $session->removeNote(self::FORM_PRINCIPAL_NOTE);

        if ($saved === null)
            return (false);

        $request->setQueryString($saved->getQueryString());
        $request->setRequestURI($saved->getRequestURI());

        return (true);

    }


    /**
     * Save the original request information into our session.
     *
     * @param request The request to be saved
     * @param session The session to contain the saved information
     */
    private function saveRequest(Request $request, Session $session) {

        require_once("aas/authenticator/SavedRequest.php");

        // Create and populate a SavedRequest object for this request
        $saved = new SavedRequest();
        $saved->setMethod($request->getMethod());
        $saved->setQueryString($request->getQueryString());
        $saved->setRequestURI($request->getRequestURI());

        // Stash the SavedRequest in our session for later use
        $session->setNote(self::FORM_REQUEST_NOTE, $saved);

    }


    /**
     * Return the request URI (with the corresponding query string, if any)
     * from the saved request so that we can redirect to it.
     *
     * @param session Our current session
     */
    private function savedRequestURL(Session $session) {

        require_once("aas/authenticator/SavedRequest.php");
        $saved = $session->getNote(self::FORM_REQUEST_NOTE);

        if ($saved === null) {
            if($this->log->isDebugEnabled())
                $this->log->debug("Saved request not found");
            return (null);
        }

        $sb = $saved->getRequestURI();
        if ($saved->getQueryString() !== null) {
            $sb .= '?';
            $sb .= $saved->getQueryString();
        }

        if($this->log->isDebugEnabled())
            $this->log->debug("Saved Request URL : ".$sb);

        return ($sb);

    }


}
?>